package metrics

import (
	"fmt"
	"time"

	"github.com/modern-go/concurrent"

	"github.com/prometheus/client_golang/prometheus"
)

type (
	operation struct {
		successes *prometheus.CounterVec
		failures  *prometheus.CounterVec
		duration  *prometheus.HistogramVec
	}

	Supplier[T any] func() (T, error)
)

var (
	cache          *concurrent.Map
	defaultBuckets = []float64{20.0, 50.0, 80.0, 120.0, 180.0, 240.0, 360.0, 480.0, 600.0, 780.0}
)

func init() {
	cache = concurrent.NewMap()
}

func WithSupplier[T any](operation, product, service string, block Supplier[T]) (T, error) {
	var result T
	var err error

	err = WithOperation(operation, product, service, func() error {
		result, err = block()
		return err
	})

	return result, err
}

func WithOperation(operation, product, service string, block func() error) error {
	if operation == "" {
		panic(fmt.Errorf("operation must be not empty"))
	}

	if product == "" {
		panic(fmt.Errorf("operation must be not empty"))
	}

	if service == "" {
		panic(fmt.Errorf("operation must be not empty"))
	}

	ops := getOrCreateOperation(product, service)
	start := time.Now()

	defer func() {
		ops.duration.WithLabelValues(operation).Observe(time.Since(start).Seconds())

		recovered := recover()
		if recovered != nil {
			ops.failures.WithLabelValues(operation).Inc()
			panic(recovered)
		}
	}()

	err := block()
	if err != nil {
		ops.failures.WithLabelValues(operation).Inc()
		return err
	}

	ops.successes.WithLabelValues(operation).Desc()

	return nil
}

func getOrCreateOperation(product string, service string) *operation {
	constLabels := map[string]string{"product": product, "service": service}
	varLabels := []string{"operation"}

	value, loaded := cache.LoadOrStore(fmt.Sprintf("%s:%s", product, service), &operation{
		successes: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name:        "operation_success_counter",
			Help:        "Total number of success operations",
			ConstLabels: constLabels,
		}, varLabels),
		failures: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name:        "operation_failure_counter",
			Help:        "Total number of failure operations",
			ConstLabels: constLabels,
		}, varLabels),
		duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
			Name:        "operation_duration_seconds",
			Help:        "Total number of failure operations",
			Buckets:     defaultBuckets,
			ConstLabels: constLabels,
		}, varLabels),
	})

	ops := value.(*operation)

	if !loaded {
		prometheus.MustRegister(ops.successes)
		prometheus.MustRegister(ops.failures)
		prometheus.MustRegister(ops.duration)
	}

	return ops
}
